/*              Copyright(c) 2008 Dave Sexton              *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.ComponentModel;
using System.IO;
using System.Xml;
using System.Xml.XPath;

namespace DaveSexton.Sandcastle
{
	/// <summary>
	/// Encapsulates build component state that can be used by <see cref="ResolveExternalLinksComponent"/> and its graphical 
	/// <see cref="ResolveExternalLinksComponent.Editor">Editor</see>.
	/// </summary>
	/// <remarks>
	/// Add properties to this class that use the <see cref="Navigator"/> property (defined in the designer file) 
	/// to strong-type configuration data for the component.  Helper methods such as <see cref="ReadAttribute"/>
	/// and <see cref="WriteAttribute"/> are defined in the partial class.
	/// </remarks>
	internal sealed partial class ResolveExternalLinksComponentConfiguration
	{
		#region Public Properties
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1802:UseLiteralsWhereAppropriate")]
		public static readonly string DefaultSeeXPath = "//see[@xref|@vref]";
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1802:UseLiteralsWhereAppropriate")]
		public static readonly string DefaultSeeAlsoXPath = "//seealso[@xref|@vref]";

		/// <summary>
		/// Indicates whether the properties of this class can be displayed in a property grid control and other 
		/// types of graphical editors within a host application.
		/// </summary>
		/// <remarks>This field is used by <see cref="ResolveExternalLinksComponent"/>'s type converter (in the designer file).</remarks>
		public const bool PropertiesSupported = true;

		/// <summary>
		/// Gets or sets whether the component should perform its primary function during builds.
		/// </summary>
		/// <remarks>
		/// This property is not required by build component implementations.  It merely serves as a working
		/// example for encapsulating component configuration in a separate class, with properties that can 
		/// be used by an editor control for reading and writing settings in the Build Assembler configuration
		/// file.
		/// </remarks>
		[DisplayName("Enabled"), DefaultValue(true)]
		[Description("Specifies whether the component will actively process topics.")]
		public bool Enabled
		{
			get
			{
				bool result;
				if (bool.TryParse(ReadAttribute("enabled"), out result))
					return result;
				else
					return true;		// default value
			}
			set
			{
				WriteAttribute("enabled", (!value) ? value.ToString() : null);
			}
		}

		[DisplayName("Base URI"), DefaultValue("")]
		[Description("Specifies the default base URI for all mappings that do not override this setting.")]
		public string BaseUri
		{
			get
			{
				return ReadAttribute("base") ?? "";
			}
			set
			{
				WriteAttribute("base", value);
			}
		}

		[DisplayName("Target"), DefaultValue("")]
		[Description("Specifies the default hyperlink target for all mappings that do not override this setting.")]
		[TypeConverter(typeof(TargetTypeConverter))]
		public string Target
		{
			get
			{
				return ReadAttribute("target") ?? "";
			}
			set
			{
				WriteAttribute("target", value);
			}
		}

		[DisplayName("Topic mapping scopes count")]
		[Description("The total number of unqiue mapping scopes based on topic ID.")]
		public int MappingScopesCount
		{
			get
			{
				return mappingScopes.Count;
			}
		}

		[DisplayName("See XPath"), DefaultValue("//see[@xref|@vref]")]
		[Description("The XPath used to match links in the source document.")]
		public string SeeXPath
		{
			get
			{
				return ReadAttribute("seeXPath") ?? DefaultSeeXPath;
			}
			set
			{
				WriteAttribute("seeXPath", (value != DefaultSeeXPath) ? value : null);
			}
		}

		[DisplayName("See link XML"), DefaultValue(null)]
		[Description("The XML format string that replaces matching see links in the source document.  " +
								 "Three arguments are supported: {0} = href; {1} = target; {2} = text.  " +
								 "By default, an HTML anchor is created when the value of this property is null or empty.")]
		[Editor(typeof(System.ComponentModel.Design.MultilineStringEditor), typeof(System.Drawing.Design.UITypeEditor))]
		public string SeeLinkXml
		{
			get
			{
				XPathNavigator seeLinkXml = Navigator.SelectSingleNode("seeLinkXml");

				if (seeLinkXml != null)
					return seeLinkXml.InnerXml.Trim();
				else
					return null;
			}
			set
			{
				XPathNavigator seeLinkXml = Navigator.SelectSingleNode("seeLinkXml");

				if (string.IsNullOrEmpty(value))
				{
					if (seeLinkXml != null)
						seeLinkXml.DeleteSelf();
				}
				else
				{
					if (seeLinkXml == null)
					{
						seeLinkXml = Navigator.Clone();
						seeLinkXml.PrependChildElement(null, "seeLinkXml", null, null);
						seeLinkXml.MoveToFirstChild();
					}

					seeLinkXml.InnerXml = value;
				}
			}
		}

		[DisplayName("See also XPath"), DefaultValue("//seealso[@xref|@vref]")]
		[Description("The XPath used to match see also links in the source document.")]
		public string SeeAlsoXPath
		{
			get
			{
				return ReadAttribute("seeAlsoXPath") ?? DefaultSeeAlsoXPath;
			}
			set
			{
				WriteAttribute("seeAlsoXPath", (value != DefaultSeeAlsoXPath) ? value : null);
			}
		}

		[DisplayName("Replace see also links"), DefaultValue(true)]
		[Description("The XPath used to match links in the source document.")]
		public bool ReplaceSeeAlsoLinks
		{
			get
			{
				bool result;
				if (bool.TryParse(ReadAttribute("replaceSeeAlsoLinks"), out result))
					return result;
				else
					return true;		// default value
			}
			set
			{
				WriteAttribute("replaceSeeAlsoLinks", (!value) ? value.ToString() : null);
			}
		}

		[Browsable(false)]
		public int MappingCount
		{
			get
			{
				return mappingCount;
			}
		}

		[Browsable(false)]
		public string SettingsDirectory
		{
			get
			{
				return (Host != null) ? Host.SettingsDirectory : null;
			}
		}

		[Browsable(false)]
		public string CurrentDirectory
		{
			get
			{
				return ((Host != null) ? Host.CurrentDirectory : null)
					?? Environment.CurrentDirectory;
			}
		}

		[Browsable(false)]
		public ApiReferenceCollection ApiReferences
		{
			get
			{
				return (Host != null) ? Host.ApiReferences : null;
			}
		}

		[Browsable(false)]
		public XmlNamespaceManager NamespaceManager
		{
			get
			{
				XmlNamespaceManager ns = new XmlNamespaceManager(new NameTable());

				foreach (XPathNavigator navigator in Navigator.SelectChildren("context", ""))
				{
					string name = navigator.GetAttribute("name", "");

					if (!string.IsNullOrEmpty(name))
						ns.AddNamespace(navigator.GetAttribute("prefix", ""), name);
				}

				return ns;
			}
		}
		#endregion

		#region Private / Protected
		private int mappingCount;
		private readonly Dictionary<string, List<UrlMappingGroup>> mappingScopes = new Dictionary<string, List<UrlMappingGroup>>(8);
		#endregion

		#region Methods
		/// <summary>
		/// Initializes the class for use with the current XML configuration, during builds and as a fallback while editing.
		/// </summary>
		/// <remarks>
		/// This method is invoked when the build component is being executed and when the host application is unknown 
		/// while editing; otherwise, a host-specific initialization method is invoked instead, such as 
		/// <see cref="InitializeForDocProject"/>.
		/// <para>
		/// The host is unknown if it doesn't provide a well-known service that can be consumed.
		/// </para>
		/// <para>
		/// The implementation of this method must use global configuration since it cannot be determined during builds 
		/// whether the host supports projects and where, if so, the project is located on disc.  That is because the 
		/// host is not reachable during builds.
		/// </para>
		/// </remarks>
		private void Initialize()
		{
			ForEachMappingGroup(delegate(XPathNavigator mapping)
			{
				if (Host == null)
					// Only load URL mappings during builds, when the Host is unavailable.  This way mappings aren't loaded each time
					// the configuration is required in the component's editor.
					AddMappingGroup(UrlMappingGroup.CreateFrom(CurrentDirectory, mapping.ReadSubtree(), LoadMappingAction));
				else
					AddMappingGroup(UrlMappingGroup.CreateFrom(CurrentDirectory, mapping.ReadSubtree()));
			});
		}

		public void ForEachMappingGroup(Action<XPathNavigator> action)
		{
			if (action == null)
				throw new ArgumentNullException("action");

			foreach (XPathNavigator mapping in Navigator.SelectChildren("mappings", ""))
				action(mapping);
		}

		public void AddMappingGroup(UrlMappingGroup map)
		{
			List<UrlMappingGroup> mappings;

			if (mappingScopes.ContainsKey(map.TopicId ?? ""))
				mappings = mappingScopes[map.TopicId ?? ""];
			else
			{
				mappings = new List<UrlMappingGroup>(32);
				mappingScopes.Add(map.TopicId ?? "", mappings);
			}

			mappings.Add(map);
		}

		private void LoadMappingAction(UrlMappingReference map)
		{
			mappingCount++;

			string topicId = map.Group.TopicId;

			if (mappingScopes.ContainsKey(topicId ?? ""))
			{
				foreach (UrlMappingGroup storedMap in mappingScopes[topicId ?? ""])
				{
					string id = map.Mapping.Id;

					if (storedMap.HasMapping(id))
						throw new InvalidOperationException(string.Format(System.Globalization.CultureInfo.CurrentCulture,
							"The same URL identifier may only appear once per topic mappings group.{0}Topic ID: {1}{0}URL ID: {2}",
							Environment.NewLine, topicId, id));
				}
			}
		}

		public UrlMappingReference GetMapping(string topicId, string id)
		{
			if (mappingScopes.ContainsKey(topicId ?? ""))
			{
				UrlMappingReference mapping = GetMappingExact(topicId ?? "", id);

				if (mapping != null)
					return mapping;
			}

			if (!string.IsNullOrEmpty(topicId) && mappingScopes.ContainsKey(""))
				// Look for a match from within the general group of mappings that aren't associated with any particular topic
				// since the topicId was set but no match was found.
				return GetMappingExact("", id);
			else
				return null;
		}

		private UrlMappingReference GetMappingExact(string topicId, string id)
		{
			foreach (UrlMappingGroup group in mappingScopes[topicId])
				if (group.HasMapping(id))
					return new UrlMappingReference(group, new UrlMapping(id, group.GetMapping(id)));

			return null;
		}
		#endregion

		#region Nested
		private sealed class TargetTypeConverter : TypeConverter
		{
			#region Public Properties
			#endregion

			#region Private / Protected
			private static StandardValuesCollection standardValues;
			#endregion

			#region Constructors
			/// <summary>
			/// Constructs a new instance of the <see cref="TargetTypeConverter" /> class.
			/// </summary>
			public TargetTypeConverter()
			{
			}
			#endregion

			#region Methods
			public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
			{
				return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
			}

			public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
			{
				if (value is string)
					return value;
				else
					return base.ConvertFrom(context, culture, value);
			}

			public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
			{
				return true;
			}

			public override bool GetStandardValuesExclusive(ITypeDescriptorContext context)
			{
				return false;
			}

			public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
			{
				if (standardValues == null)
				{
					standardValues = new StandardValuesCollection(new string[] {
							"_blank", "_parent", "_self", "_top"
						});
				}

				return standardValues;
			}
			#endregion
		}
		#endregion
	}
}
